/*

   **********************************************************************
   *                                                                    *
   * This file contains the code for an image object for a content-     *
   * based image retrieval application that "summarizes" an image and   *
   * sends the resulting image "signature" to a server for              *
   * classification. The server returns the classification result in    *
   * the form of a Web page containing example images of the input      *
   * image's category.                                                  *
   *                                                                    *
   * The image class defined here is rather specific to this particular *
   * application and is not really designed for broad reuse (just yet). *
   *                                                                    *
   **********************************************************************
*/

/*
   **********************************************************************
   *                                                                    *
   * Gist   -- content-based image retrieval application for the gPhone *
   *                                                                    *
   * Copyright (C) 2011 Yong Zeng                                       *
   *                                                                    *
   * This file is part of Gist.                                       *
   *                                                                    *
   * Gist is free software; you can redistribute it and/or modify it  *
   * under the terms of the GNU General Public License as published by  *
   * the Free Software Foundation; either version 2 of the License, or  *
   * (at your option) any later version.                                *
   *                                                                    *
   * Gist is distributed in the hope that it will be useful, but      *
   * WITHOUT ANY WARRANTY; without even the implied warranty of         *
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU   *
   * General Public License for more details.                           *
   *                                                                    *
   * You should have received a copy of the GNU General Public License  *
   * along with Goggle; if not, write to the Free Software Foundation,  *
   * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.       *
   *                                                                    *
   **********************************************************************
*/

//----------------------- PACKAGE SPECIFICATION -------------------------

package vision.gistcomputing ;

//------------------------------ IMPORTS --------------------------------

// Android UI support
import android.widget.Toast ;
import android.app.AlertDialog ;

// Android content provider support
import android.provider.MediaStore.Images ;
import android.content.ContentResolver ;

// Android graphics support
import android.graphics.BitmapFactory ;
import android.graphics.Bitmap ;

// Android networking support
import android.net.Uri ;

// Android database support
import android.database.Cursor ;

// Android application and OS support
import android.app.Activity ;
import android.content.Context ;
import android.content.Intent ;
import android.os.Handler ;
import android.os.Message ;

// Android utilities
import android.content.ContentUris ;
import android.util.Log ;

// Java I/O support
import java.io.PrintStream ;
import java.io.ByteArrayOutputStream ;
import java.io.ByteArrayInputStream ;
import java.io.FileOutputStream ;
import java.io.InputStream ;
import java.io.File ;
import java.io.FilenameFilter ;
import java.io.FileNotFoundException ;
import java.io.IOException ;

import vision.gistcomputing.R;

//------------------------- CLASS DEFINITION ----------------------------

/**
   This class provides several handy utility functions.
*/
class Utils {

//------------------------- FUNCTION OBJECTS ----------------------------

/// A quick interface for implementing function objects.
public interface Function {
   Object apply(Object O) ;
}

/// A dummy base class for providing a common interface for different
/// kinds of bitmap decoders.
public static abstract class BitmapDecoder implements Function {
}

/// This bitmap decoder decodes Android bitmaps from byte arrays.
public static class ByteArrayBitmapDecoder extends BitmapDecoder {
   byte[] m_array ;

   public ByteArrayBitmapDecoder(byte[] array) {
      m_array = array ;
   }

   public Object apply(Object O) {
      return BitmapFactory.decodeByteArray(m_array, 0, m_array.length,
                                           (BitmapFactory.Options) O) ;
   }
} // end of Utils.ByteArrayBitmapDecoder

/// This bitmap decoder decodes Android bitmaps from image files.
public static class FileBitmapDecoder extends BitmapDecoder {
   String m_name ;

   public FileBitmapDecoder(String file_name) {
      m_name = file_name ;
   }

   public Object apply(Object O) {
      return BitmapFactory.decodeFile(m_name, (BitmapFactory.Options) O) ;
   }
} // end of Utils.FileBitmapDecoder

/// This bitmap decoder decodes Android bitmaps from input streams. Since
/// the load_bitmap() function, in conjunction with which this decoder is
/// used, first determines the size of the bitmap from the decoder's
/// input source and then loads the bitmap, applying appropriate scaling
/// factors, the input stream will need to be rewound every time this
/// function object is applied.
///
/// Therefore, clients must supply this decoder with a stream rewinding
/// function object.
public static class InputStreamBitmapDecoder extends BitmapDecoder {
   InputStream m_is ;
   Function    m_rewinder ;

   public InputStreamBitmapDecoder(InputStream is, Function stream_rewinder) {
      m_is = is ;
      m_rewinder = stream_rewinder ;
   }

   public Object apply(Object O) {
      Bitmap B =
         BitmapFactory.decodeStream(m_is, null, (BitmapFactory.Options) O) ;
      m_is = (InputStream) m_rewinder.apply(m_is) ;
      return B ;
   }
} // end of Utils.InputStreamBitmapDecoder

//-------------------------- IMAGE UTILITIES ----------------------------

/// Return ID of full-sized image corresponding to specified thumbnail
public static final int full_picture_id(Activity A, long thumbnail_id)
{
   try
   {
      String[] columns = new String[] {
         Images.Thumbnails._ID,
         Images.Thumbnails.IMAGE_ID,
      } ;
      String where_clause = Images.Thumbnails._ID + "=" + thumbnail_id ;
      Cursor C = A.managedQuery(Images.Thumbnails.EXTERNAL_CONTENT_URI,
                                columns, where_clause, null, null) ;
      if (C.getCount() > 0) {
         C.moveToFirst() ;
         return C.getInt(C.getColumnIndex(Images.Thumbnails.IMAGE_ID)) ;
      }
   }
   catch (android.database.sqlite.SQLiteException e)
   {
      Log.e(null, "MVN: unable to retrieve thumbnail ID " + thumbnail_id, e) ;
   }
   return -1 ;
}

/// Return the thumbnail ID of the specified picture given its string URI
public static final long get_thumbnail_id(Activity A, String uri)
{
   return get_thumbnail_id(A, ContentUris.parseId(Uri.parse(uri))) ;
}

/// Return the thumbnail ID of the specified picture given its ID
public static final long get_thumbnail_id(Activity A, long image_id)
{
   try
   {
      String[] columns = new String[] {
         Images.Thumbnails._ID,
      } ;
      String where_clause = Images.Thumbnails.IMAGE_ID + "=" + image_id ;
      Cursor C = A.managedQuery(Images.Thumbnails.EXTERNAL_CONTENT_URI,
                                columns, where_clause, null, null) ;
      if (C.getCount() > 0) {
         C.moveToFirst() ;
         return C.getInt(C.getColumnIndex(Images.Thumbnails._ID)) ;
      }
   }
   catch (Exception e)
   {
      Log.e(null, "MVN: unable to get thumbnail for image " + image_id, e) ;
   }
   return -1 ;
}

/// Load the image given its ID.
public static final
InputStream load_image(Context C, long id) throws FileNotFoundException
{
   return C.getContentResolver().openInputStream(ContentUris.
             withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id)) ;
}

/// This function loads an Android bitmap using the supplied bitmap
/// decoder. The loaded bitmaps are restricted to the specified size.
public static final
Bitmap load_bitmap(int W, int H, BitmapDecoder bitmap_decoder)
{
   // First, determine the size of the bitmap to be decoded
   BitmapFactory.Options options = new BitmapFactory.Options() ;
   options.inJustDecodeBounds = true ;
   bitmap_decoder.apply(options) ;
   if (options.outWidth <= 0 || options.outHeight <= 0)
      throw new RuntimeException("unable to decode bitmap") ;
   Log.e("Goggle", "In Utils.load_bitmap()...") ;
   Log.e("Goggle", "actual  size = " +
                   options.outWidth  + "x" + options.outHeight) ;
   Log.e("Goggle", "desired size = " + W + "x" + H) ;

   // Then, read the image from the decoder's input source, applying a
   // crude scaling factor that produces a bitmap of approximately the
   // desired size.
   options.inJustDecodeBounds = false ;
   options.inSampleSize = Math.min(options.outWidth/W, options.outHeight/H) ;
   Bitmap B = (Bitmap) bitmap_decoder.apply(options) ;
   if (B == null)
      throw new RuntimeException("unable to decode bitmap") ;
   Log.e("Goggle", "scaled  size = " + B.getWidth()  + "x" + B.getHeight()) ;
   Log.e("Goggle", "scale factor = " + options.inSampleSize) ;

   // Finally, rescale the bitmap obtained above to the exact desired size
   if (options.outWidth != W || options.outHeight != H) {
      B = Bitmap.createScaledBitmap(B, W, H, true) ;
      Log.e("Goggle", "rescale size = " + B.getWidth() + "x" + B.getHeight()) ;
   }
   return B ;
}

/// Import an image file into the Media database.
public static final
void import_image(Context C, String file_name, int W, int H)
{
   Bitmap B  = load_bitmap(W, H, new FileBitmapDecoder(file_name)) ;
   file_name = basename(file_name) ;
   Images.Media.insertImage(C.getContentResolver(), B, file_name, file_name) ;
}

/// Convert the given byte array into an Android bitmap of the specified
/// size.
public static final Bitmap array_to_bitmap(byte[] data, int W, int H)
{
   return load_bitmap(W, H, new ByteArrayBitmapDecoder(data)) ;
}

/// Delete a picture and its thumbnails given its URI in string form
public static final void delete_picture(Activity A, String uri)
{
   A.getContentResolver().delete(Uri.parse(uri), null, null) ;
}

/// Delete a picture and its thumbnails given its ID
public static final void delete_picture(Activity A, long id)
{
   A.getContentResolver().delete(ContentUris.withAppendedId(
      Images.Media.EXTERNAL_CONTENT_URI, id), null, null) ;
}

/// Delete all the pictures in the Android MediaStore whose titles match
/// the supplied pattern.
public static final void delete_all_pictures(Activity A)
{
   try
   {
      String[] columns = new String[] {
         Images.Media._ID,
      } ;
      Cursor C = A.managedQuery(Images.Media.EXTERNAL_CONTENT_URI,
                                columns, null, null, null) ;
      int[] image_ids = extract_ids_from_cursor(C) ;
      C.close() ;

      if (image_ids == null)
         return ;
      for (int i = 0; i < image_ids.length; ++i)
         delete_picture(A, image_ids[i]) ;
   }
   catch (Exception e)
   {
      Log.e(null, "MVN: unable to delete all images", e) ;
   }
}

/// A helper routine that returns the image IDs held by the supplied
/// cursor.
///
/// WARNING: This routine is not a generic means of extracting database
/// cursor fields to an array. It is very specific to this photo manager
/// application and is meant to be used only by the delete_all_pictures
/// function defined above.
private static final int[] extract_ids_from_cursor(Cursor C)
{
   if (C == null || C.getCount() <= 0)
      return null ;

   int[] ids = new int[C.getCount()] ;
   C.moveToFirst() ;
   for (int i = 0; i < ids.length; ++i, C.moveToNext())
      ids[i] = C.getInt(0) ;
   return ids ;
}

//------------------------- UI NOTIFICATIONS ----------------------------

/// A short notification message that doesn't steal focus or require any
/// specific interaction on the user's part to dismiss. It simply appears
/// briefly and fades away.
public static final void notify(Context C, String msg)
{
   Toast.makeText(C, msg, Toast.LENGTH_SHORT).show() ;
}

/// Long notification message
public static final void notify_long(Context C, String msg)
{
   Toast.makeText(C, msg, Toast.LENGTH_LONG).show() ;
}

/// Show an error box
public static final void alert(Context C, String msg)
{
   AlertDialog.Builder alert = new AlertDialog.Builder(C) ;
   alert.setMessage(msg) ;
   alert.setPositiveButton(R.string.alert_okay_label, null) ;
   alert.show() ;
}

//----------------------- THREAD NOTIFICATIONS --------------------------

/// Send a message to an activity or service's main thread.
public static final void notify_main_thread(Handler H, int msg_id)
{
   Message M = Message.obtain() ;
   M.what = msg_id ;
   H.sendMessage(M) ;
}

/// Send a message to an activity or service's main thread. The message
/// will include the supplied integer data.
public static final void notify_main_thread(Handler H, int msg_id, int data)
{
   Message M = Message.obtain() ;
   M.what = msg_id ;
   M.arg1 = data ;
   H.sendMessage(M) ;
}

/// Send a message to an activity or service's main thread. The message
/// will include the supplied string data.
public static final void notify_main_thread(Handler H, int msg_id, String data)
{
   Message M = Message.obtain() ;
   M.what = msg_id ;
   M.obj  = data ;
   H.sendMessage(M) ;
}

//----------------------- FILE SYSTEM FUNCTIONS -------------------------

/// Performs the specified operation for each file matching a given
/// pattern.
public static final void for_each_file(final String glob, Function F)
{
   final int last_sep = glob.lastIndexOf(File.separatorChar) ;

   File dir = new File(glob.substring(0, last_sep)) ;
   String[] list = dir.list(new FilenameFilter() {
         private String pattern = new String(glob.substring(last_sep + 1));
         public  boolean accept(File dir, String file_name) {
            return file_name.matches(pattern) ;
         }
      }) ;

   final int N = list.length ;
   for (int i = 0; i < N; ++i)
      F.apply(dir.getPath() + File.separator + list[i]) ;
}

/// Quick helper to extract just the file name from a given path name (in
/// case the user entered a full path, which Android does not allow).
public static final String basename(String path_name)
{
   return new File(path_name).getName() ;
}

/// This function returns true if the specified file exists, is readable
/// and actually has some data in it; false otherwise.
public static final boolean exists(String file_name)
{
   File f = new File(file_name) ;
   return f.exists() && f.canRead() && f.length() > 0 ;
}

/// This function removes the specified file.
public static final void unlink(String file_name)
{
   new File(file_name).delete() ;
}

/// Remove all files matching specified pattern.
public static final void unlink_all(final String glob)
{
   Function delete_it = new Function() {
         public Object apply(Object file_name) {
            Utils.unlink((String) file_name) ;
            return null ;
         }} ;
   for_each_file(glob, delete_it) ;
}

/// This function converts an array to an in-core input stream.
public static final InputStream array_to_input_stream(float[] array)
{
   ByteArrayOutputStream os = new ByteArrayOutputStream() ;
   PrintStream out = new PrintStream(os) ;
   for (int i = 0; i < array.length; ++i)
      out.println(array[i]) ;
   return new ByteArrayInputStream(os.toByteArray()) ;
}

/// Store the given float array to the specified file.
private static final
void array_to_output_stream(float[] array, PrintStream out)
{
   final int N = array.length ;
   for  (int i = 0; i < N; ++i)
      out.print(array[i] + " ") ;
   out.print("\n") ;
}

/// Store the given float array to the specified file.
public static final void save(float[] array, String file_name)
{
   try
   {
      PrintStream out = new PrintStream(new FileOutputStream(file_name)) ;
      array_to_output_stream(array, out) ;
      out.close() ;
   }
   catch (IOException e)
   {
      Log.e("Goggle", "unable to save float array to " + file_name, e) ;
   }
}

/// Store the given float array to the specified file.
public static final void append(float[] array, String file_name)
{
   try
   {
      PrintStream out = new PrintStream(new FileOutputStream(file_name, true));
      array_to_output_stream(array, out) ;
      out.close() ;
   }
   catch (IOException e)
   {
      Log.e("Goggle", "unable to save float array to " + file_name, e) ;
   }
}

//---------------------------- MATH STUFF -------------------------------

/// This function normalizes an array and returns the result.
public static final float[] normalize(float[] array)
{
   float sum = 0 ;
   for (int i = 0; i < array.length; ++i)
      sum += array[i] ;
   float normalizer = 1/sum ;
   for (int i = 0; i < array.length; ++i)
      array[i] *= normalizer ;
   return array ;
}

/// This function normalizes an array using the sum of squares rather
/// than just the sum.
public static final float[] normalize2(float[] array)
{
   float sum = 0 ;
   for (int i = 0; i < array.length; ++i)
      sum += array[i] * array[i] ;

   float normalizer = 1/(float) Math.sqrt(sum) ;
   if (normalizer < 1e-10) // too small to normalize
      return array ;

   for (int i = 0; i < array.length; ++i)
      array[i] *= normalizer ;
   return array ;
}

/// This function thresholds the given array so that any values in it
/// beyond the specified limit are capped to the limit.
public static final float[] threshold(float[] array, float limit)
{
   for (int i = 0; i < array.length; ++i)
      if (array[i] > limit)
          array[i] = limit ;
   return array ;
}

/// Clamp a floating point value to lie within the range of a byte (but
/// return as an int because Java doesn't support unsigned bytes; we
/// could use the fact that negative numbers are 2's complements, but
/// that's just unnecessarily confusing).
public static final int clamp_byte(float f)
{
   if (f < 0)
      return 0 ;
   if (f > 255)
      return 255 ;
   return (int) f ;
}

//--------------------------- DEBUG SUPPORT -----------------------------

/// Dump a float array.
public static final void dump(float[] array)
{
   for (int i = 0; i < array.length; ++i)
      Log.e("Goggle", "Utils.dump(): array[" + i + "] = " + array[i]) ;
}

//-----------------------------------------------------------------------

} 
